#!/opt/local/bin/perl
#$Id: ffmpeg2iphone.pl 35 2011-10-07 21:06:57Z iceblox $

#    Copyright (C) 2009-2010 Simon Hitzemann
#
#    This program is free software; you can redistribute it and/or modify
#    it under the terms of the GNU General Public License as published by
#    the Free Software Foundation; version 2 of the License applies
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#
#    You should have received a copy of the GNU General Public License along
#    with this program; if not, write to the Free Software Foundation, Inc.,
#    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.

use common::sense;
use 5.012;
use Carp;
use File::Temp;
use File::Copy;
use Log::Log4perl qw(:easy);
use Log::Log4perl::Level;
use FindBin qw($Bin);
use Getopt::Std;
use IPC::Run3;
use XML::Simple;
use POSIX;

our %opts;
getopts( '1qdna:l:D:', \%opts );

my ($home) = glob "~";
Log::Log4perl->init("$Bin/ffmpeg2iphone.l4p");

my $logger = Log::Log4perl->get_logger();

### User Changeable Settings ###
# Where to put the final movie #
my $outdir = "$home/Movies/ffmpeg";

# Set 0 for single pass, 1 for two pass #
my $twopass = 1;

# Set this to the number of cores in your machine to speed up encoding
# -1 = autodetect;
my $numthreads = -1;

# Set to 1 to get very verbose output #
my $debug = 0;

# Set to 1 to only show what would be executed
my $rundry = 0;

# Set to 1 to grow image dimensions to even numbers and add paddign to the next multiples of 16 (speeds up encoding) #
# Set to 2 to grow image dimensions to even numbers and add padding to max dimensions (speeds up encoding) #
# Set to anything else to ignore #
my $optimize_dimensions = 1;

# utilities, change to your needs
my $ffmpeg = "ffmpeg";

# mediainfo is optional and this script will fall back to ffmpeg if mediainfo is not found
my $mediainfo = "mediainfo";

# atomicparsley is not needed, but then your videos might be not playable on the iPhone
my $atomicparsley = "AtomicParsley";

# Set the default device to use if given device is not found in XML config
my $defaultdevice = "iphone4";
my $device;
### End Of User Changeable Settings ###

### Read Config ###
my $config = XMLin( undef, ForceArray => 1, ForceContent => 1 );
### End Of Read Config ###

if ( defined( $opts{'d'} ) ) {
	$debug = 1;
	$logger->level($DEBUG);
}
if ( defined( $opts{'n'} ) ) {
	$rundry = 1;
}

if ( defined( $opts{'1'} ) ) {
	$twopass = 0;
}
if ( defined( $opts{'D'} ) ) {
	my $opt_D = $opts{'D'};
	$device = $opt_D;
	if ( !exists( $config->{devices}->{$device}->{opts} ) ) {
		$logger->info(
			"Did not find device $device, fallback to default $defaultdevice.");
		$device = $defaultdevice;
	}
} else {
	$device = $defaultdevice;
}
if ( 1 == $debug ) {
	use Data::Dumper;
	$logger->debug("Config Dump Start");
	$logger->debug( Dumper($config) );
	$logger->debug("Config Dump End");
}
( $ffmpeg, $mediainfo, $atomicparsley ) =
  checkBinaries( $ffmpeg, $mediainfo, $atomicparsley, $config, $logger );
my ($cores) = getNumCores($numthreads);

my $VERSION = "0.0.2";

my ( $nownum, $numfiles );
$nownum   = 0;
$numfiles = scalar(@ARGV);

### ONLY CHANGE IF YOU KNOW WHAT YOU ARE DOING ###
my $maxwidth   = $config->{devices}->{$device}->{xres};
my $maxheight  = $config->{devices}->{$device}->{yres};
my $vcodec     = $config->{devices}->{$device}->{codecs}->{video}->{type};
my $acodec     = $config->{devices}->{$device}->{codecs}->{audio}->{type};
my $vbitrate   = $config->{devices}->{$device}->{codecs}->{video}->{bitrate};
my $abitrate   = $config->{devices}->{$device}->{codecs}->{audio}->{bitrate};
my $ffmpegopts = $config->{devices}->{$device}->{opts};
$ffmpegopts = $ffmpegopts
    . " -vcodec ${vcodec} -acodec ${acodec} -ab ${abitrate} -b:v ${vbitrate} -threads ${cores}";

my ($opt_a) = checkAspectRatio( $logger, $ffmpegopts, \%opts );
my ($opt_l) = checkAudioLanguage( $logger, $ffmpegopts, \%opts );
my @ffmpegopts = split( / /, $ffmpegopts );
### DO NOT COMPLAIN IF YOU CHANGED THIS AND ANYTHING BREAKS ###

foreach my $input (@ARGV) {

	$nownum++;

	$logger->info("Now processing file $nownum/$numfiles: $input");

	# Create a temporary directory for all those nasty 2 pass log files
	# File::Temp cleans it up for us
	my $tmpdir;
	if ( 1 == $debug ) {
		$tmpdir = File::Temp::tempdir( CLEANUP => 1 );
	} else {
		$tmpdir = File::Temp::tempdir( CLEANUP => 0 );
	}

	# Get the current directory to return to later on
	my $curdir = qx(pwd);
	chomp $curdir;
	$logger->debug("Current dir: ${curdir}");

	# Go to the temporary directory
	$logger->debug("Changing to: ${tmpdir}");
	chdir $tmpdir;

	# Check if we have a source filename
	if ( $input =~ /^$/ ) {
		$logger->logdie("Please supply a source filename");
	}

# No matter if it's absolute or relative, plunge the old current dir in front of it
# remember we are already in the temporary directory
	my $srcmovie;
	if ( $input =~ /^\// ) {
		$srcmovie = $input;
	} else {
		$srcmovie = $curdir . '/' . $input;
	}
	my $targetmovie = $input;

# use only the filename in $targetmove so we can use relative and absolute paths
	$targetmovie =~ s/^.*\/(.*)$/$1/;

# The final movie will have the suffix m4v so iTunes recognizes it as iPhone movie
	$targetmovie =~ s/^(.*)\.[^.]*$/$1\.m4v/;
	$targetmovie = $outdir . "/" . $targetmovie;
	my @apopts;
	push( @apopts, "./tmp.m4v" );
	push( @apopts, qw(--DeepScan --iPod-uuid 1200 --overWrite) );

	my ( $srcwidth, $srcheight, $aspect ) =
	  getMovieInfo( $ffmpeg, $logger, $mediainfo, \@ffmpegopts, $srcmovie );

	my ( $dstwidth, $dstheight );
	my $padneeded = 0;
	( $dstwidth, $dstheight ) =
	  recalculateSize( $maxwidth,  $logger,    $aspect,
					   $maxheight, $srcheight, $srcwidth );

	calculatePadding( $dstheight, $maxwidth, $logger, $dstwidth, $maxheight,
					  $optimize_dimensions, \@ffmpegopts );

	if ( 1 == $twopass ) {
		$logger->debug("Using two passes");
		$logger->info("Pass 1/2...");
		if ( 0 == $rundry ) {
			if ( defined( $opts{'q'} ) ) {
				my @cmd;
				push( @cmd,
					  $ffmpeg, "-i", $srcmovie, @ffmpegopts, "-s",
					  $dstwidth . "x" . $dstheight,
					  "-an", "-pass", "1", "./tmp.m4v" );
				run3 \@cmd, \undef, \undef, \undef;
				if ( $? != 0 ) {
					$logger->logdie("Error running ffmpeg ($?)");
				}
			} else {
				system( $ffmpeg, "-i",
						$srcmovie, @ffmpegopts,
						"-s",      $dstwidth . "x" . $dstheight,
						"-an",     "-pass",
						"1",       "./tmp.m4v"
				  ) == 0
				  or $logger->logdie("Error running ffmpeg ($?)");
			}
		} else {
			$logger->info(   "Would run $ffmpeg -i $srcmovie @ffmpegopts -s "
						   . $dstwidth . "x"
						   . $dstheight
						   . " -an -pass 1 ./tmp.m4v" );
		}
		$logger->info("Pass 2/2...");
		if ( 0 == $rundry ) {
			if ( defined( $opts{'q'} ) ) {
				my @cmd;
				push( @cmd,
					  $ffmpeg, "-i", $srcmovie, @ffmpegopts, "-s",
					  $dstwidth . "x" . $dstheight,
					  "-pass", "2", "./tmp.m4v" );
				run3 \@cmd, \undef, \undef, \undef;
				if ( $? != 0 ) {
					$logger->logdie("Error running ffmpeg ($?)");
				}
			} else {
				system( $ffmpeg, "-i", $srcmovie, @ffmpegopts, "-s",
						$dstwidth . "x" . $dstheight,
						"-pass", "2", "./tmp.m4v" ) == 0
				  or $logger->logdie("Error running ffmpeg ($?)");
			}
		} else {
			$logger->info(   "Would run $ffmpeg -i $srcmovie @ffmpegopts -s "
						   . $dstwidth . "x"
						   . $dstheight
						   . " -pass 2 ./tmp.m4v" );
		}
	} else {
		$logger->debug("Using one pass");
		$logger->info("Pass 1/1...");
		if ( 0 == $rundry ) {
			if ( defined( $opts{'q'} ) ) {
				my @cmd;
				push( @cmd,
					  $ffmpeg, "-i", $srcmovie, @ffmpegopts, "-s",
					  $dstwidth . "x" . $dstheight, "./tmp.m4v" );
				run3 \@cmd, \undef, \undef, \undef;
				if ( $? != 0 ) {
					$logger->logdie("Error running ffmpeg ($?)");
				}
			} else {
				system( $ffmpeg, "-i", $srcmovie, @ffmpegopts, "-s",
						$dstwidth . "x" . $dstheight, "./tmp.m4v" ) == 0
				  or $logger->logdie("Error running ffmpeg ($?)");
			}
		} else {
			$logger->info(   "Would run $ffmpeg -i $srcmovie @ffmpegopts -s "
						   . $dstwidth . "x"
						   . $dstheight
						   . " ./tmp.m4v" );
		}
	}

	if ( !-s "./tmp.m4v" && 0 == $rundry ) {
		$logger->logdie("Something went wrong, file is 0 bytes");
	}

	if ( -x $atomicparsley ) {
		$logger->debug("Setting iPod/iPhone UUID");
		if ( 0 == $rundry ) {
			if ( defined( $opts{'q'} ) ) {
				my @cmd;
				push( @cmd, $atomicparsley, @apopts );
				run3 \@cmd, \undef, \undef, \undef;
				if ( $? != 0 ) {
					$logger->logdie("Error running AtomicParsley ($?)");
				}
			} else {
				system( $atomicparsley, @apopts ) == 0
				  or $logger->logdie("Error running AtomicParsley ($?)");
			}
		} else {
			$logger->info("Would run $atomicparsley @apopts");
		}
	} else {
		$logger->warn(
				 "Cannot find AtomicParsley, your video might not be playable");
	}

	$logger->debug("Moving ./tmp.m4v to ${targetmovie}");
	if ( 0 == $rundry ) {
		File::Copy::move( "./tmp.m4v", "${targetmovie}" )
		  or $logger->logdie("Error moving file ($!)");
	}

	chdir $curdir;

}

$logger->info("Done!");

sub recalculateSize {
	my $dstheight;
	my $maxwidth = shift;
	my $logger   = shift;
	my $aspect   = shift;
	my $dstwidth;
	my $maxheight = shift;
	my $srcheight = shift;
	my $srcwidth  = shift;

	if ( 4 / 3 <= $aspect ) {
		$logger->debug("Aspect ratio smaller or exactly 4:3");
		if ( $maxwidth < $srcwidth ) {
			$logger->debug("Scaling by width");
			$dstwidth  = $maxwidth;
			$dstheight = int( $dstwidth / $aspect );
		} else {
			$logger->debug("No scaling needed");
			$dstwidth  = $srcwidth;
			$dstheight = $srcheight;
		}
	} else {
		$logger->debug("Aspect ratio larger than 4:3");
		if ( $maxheight < $srcheight ) {
			$logger->debug("Scaling by height");
			$dstheight = $maxheight;
			$dstwidth  = int( $dstheight * $aspect );
		} else {
			$logger->debug("No scaling needed");
			$dstwidth  = $srcwidth;
			$dstheight = $srcheight;
		}
	}
	return ( $dstwidth, $dstheight );
}

sub getMovieInfo {
	my $ffmpeg    = shift;
	my $logger    = shift;
	my $mediainfo = shift;
	my $srcheight;
	my $srcwidth;
	my $ffmpegopts = shift;
	my $srcmovie   = shift;

	if ( -x $mediainfo ) {
		$logger->debug("Found mediainfo, getting dimensions and audio codec");
		$srcwidth = qx(${mediainfo} --Inform=Video\\\;\%Width\% "${srcmovie}")
		  or $logger->logdie("Error running mediainfo");
		$srcheight = qx(${mediainfo} --Inform=Video\\\;\%Height\% "${srcmovie}")
		  or $logger->logdie("Error running mediainfo");
		chomp $srcwidth;
		chomp $srcheight;
		my $srcacodec =
		  qx(${mediainfo} --Inform=Audio\\\;\%Codec\% "${srcmovie}")
		  or $logger->logdie("Error running mediainfo");
		chomp $srcacodec;
		if ( $srcacodec =~ /^163$/ ) {
			$logger->logdie("WMA Lossless Audio Codec not supported right now");
		}
		my $framerate =
		  qx(${mediainfo} --Inform=Video\\\;\%FrameRate\% "${srcmovie}")
		  or $logger->logdie("Error running mediainfo");
		chomp $framerate;
		$framerate = "-r " . $framerate;
		push( @$ffmpegopts, split( / /, $framerate ) );
	} elsif ( -x $ffmpeg ) {
		$logger->info("Cannot find mediainfo binary, trying ffmpeg");
		my $templine = qx(${ffmpeg} -i "${srcmovie}" 2>&1);
		if ( $templine =~ m/.*Video:.* (\d+x\d+)[, ].*/ ) {
			$templine = $1;
			( $srcwidth, $srcheight ) = split( /x/, $templine );
			undef $templine;
		} else {
			$logger->logdie(
"Cannot get video size. Please check ${ffmpeg} -i ${srcmovie} output and file a bug if needed."
			);
		}
	} else {
		$logger->logdie(
"Cannot find mediainfo and ffmpeg. Please file a bug if you reach this :)" );
	}
	$logger->debug("Source dimensions: ${srcwidth}x${srcheight}");
	my $aspect = $srcwidth / $srcheight;
	$logger->debug("Aspect ratio: ${aspect}");
	return ( $srcwidth, $srcheight, $aspect );
}

sub getNumCores {
	my $numthreads = shift;

	# If this is OS X (or Darwin) check how many cores we have
	if ( $^O =~ /^darwin$/ && -1 == $numthreads ) {
		my $cores = qx(sysctl machdep.cpu.core_count);
		chomp $cores;
		$cores =~ s/^.*: (\d+)/$1/g;
		$numthreads = $cores;
	} else {

		# As a fallback use only a single thread
		$numthreads = 1;
	}
	return ($numthreads);
}

sub checkAspectRatio {
	my $logger     = shift;
	my $ffmpegopts = shift;
	my $opts       = shift;

	if ( defined( $opts->{'a'} ) ) {
		my $opt_a = $opts->{'a'};
		if ( $opt_a =~ /^4:3$/ ) {
			$logger->info("Forcing 4:3 PAR");
			$ffmpegopts = $ffmpegopts . " -aspect 4:3";
		} elsif ( $opt_a =~ /^16:9$/ ) {
			$logger->info("Forcing 16:9 PAR");
			$ffmpegopts = $ffmpegopts . " -aspect 16:9";
		}
	}
	return ($opt_a);
}

sub checkAudioLanguage {
	my $logger     = shift;
	my $ffmpegopts = shift;
	my $opts       = shift;

	if ( defined( $opts->{'l'} ) ) {
		my $opt_l = $opts->{'l'};
		$logger->info("Using alang $opt_l");
		$ffmpegopts = $ffmpegopts . " -alang " . $opt_l;
	}
	return ($opt_l);
}

sub checkBinaries {
	my $ffmpeg        = shift;
	my $mediainfo     = shift;
	my $atomicparsley = shift;
	my $config        = shift;
	my $logger        = shift;
	my @paths;
	$ffmpeg = $config->{paths}->{ffmpeg}->{path};
	if ( !-x $ffmpeg ) {
		$logger->debug("No ffmpeg binary at ${ffmpeg}.");
		@paths = grep { -x "$_/ffmpeg" } split( ':', $ENV{'PATH'} );
		if ( 0 < scalar(@paths) ) {
			$ffmpeg = $paths[0] . '/ffmpeg';
			$logger->debug("Found ffmpeg at $ffmpeg");
		}
	}
	$mediainfo = $config->{paths}->{mediainfo}->{path};
	if ( !-x $mediainfo ) {
		$logger->debug("No mediainfo binary at ${mediainfo}.");
		@paths = grep { -x "$_/mediainfo" } split( ':', $ENV{'PATH'} );
		if ( 0 < scalar(@paths) ) {
			$mediainfo = $paths[0] . '/mediainfo';
			$logger->debug("Found mediainfo at $mediainfo");
		}
	}
	$atomicparsley = $config->{paths}->{atomicparsley}->{path};
	if ( !-x $atomicparsley ) {
		$logger->debug("No AtomicParsley binary at ${atomicparsley}.");
		@paths = grep { -x "$_/AtomicParsley" } split( ':', $ENV{'PATH'} );
		if ( 0 < scalar(@paths) ) {
			$atomicparsley = $paths[0] . '/AtomicParsley';
			$logger->debug("Found AtomicParsley at $atomicparsley");
		}
	}
	if ( !-x $ffmpeg ) {
		$logger->logdie("Please install ffmpeg before running this.");
	}
	return ( $ffmpeg, $mediainfo, $atomicparsley );
}

sub calculatePadding {
	my $dstheight           = shift;
	my $maxwidth            = shift;
	my $logger              = shift;
	my $dstwidth            = shift;
	my $maxheight           = shift;
	my $optimize_dimensions = shift;
	my $ffmpegopts          = shift;

        if (0 != $dstwidth % 16) {
            $logger->debug("Destination width not a multiple of 16, this would degrade performance.");
            $dstwidth = (floor(($dstwidth/16))+1)*16;
        }
        if (0 != $dstheight % 16) {
            $logger->debug("Destination height not a multiple of 16, this would degrade performance.");
            $dstheight = (floor(($dstheight/16))+1)*16;
        }

	my $padopts = "-vf pad=${dstwidth}:${dstheight}:(ow-iw)/2:(oh-ih)/2";
        $logger->debug("Setting padding to: ${dstwidth}x${dstheight}");
	push( @$ffmpegopts, split( / /, $padopts ) );
	return;
}
